/*
 * Copyright 2015 Victor Albertos
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.rx_cache.internal;

import io.rx_cache.internal.encrypt.FileEncryptor;
import lib.jolyglot.api.JolyglotGenerics;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;

/**
 * Save objects in disk and evict them too. It uses Gson as json parser.
 */
public final class Disk implements Persistence {
    private static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "delete_tag");
    private final File cacheDirectory;
    private final FileEncryptor fileEncryptor;
    private final JolyglotGenerics jolyglot;

    @Inject
    public Disk(File cacheDirectory, FileEncryptor fileEncryptor, JolyglotGenerics jolyglot) {
        this.cacheDirectory = cacheDirectory;
        this.fileEncryptor = fileEncryptor;
        this.jolyglot = jolyglot;
    }

    /**
     * Save in disk the Record passed.
     *
     * @param key         the key whereby the Record could be retrieved/deleted later. @see evict and @see
     *                    retrieve.
     * @param record      the record to be persisted.
     * @param isEncrypted If the persisted record is encrypted or not.
     * @param encryptKey  The key used to encrypt/decrypt the record to be persisted.
     */
    @Override
    public void saveRecord(String key, Record record, boolean isEncrypted,
                           String encryptKey) {
        save(key, record, isEncrypted, encryptKey);
    }

    /**
     * allKeys
     * Retrieve the names from all files in dir
     *
     * @return List<String>
     */
    @Override
    public List<String> allKeys() {
        List<String> nameFiles = new ArrayList<>();

        File[] files = cacheDirectory.listFiles();
        if (files == null) return nameFiles;

        for (File file : files) {
            if (file.isFile()) {
                nameFiles.add(file.getName());
            }
        }

        return nameFiles;
    }

    /**
     * storedMB
     * Retrieve records accumulated memory in megabyte
     *
     * @return int
     */
    @Override
    public int storedMB() {
        long bytes = 0;

        final File[] files = cacheDirectory.listFiles();
        if (files == null) return 0;

        for (File file : files) {
            bytes += file.length();
        }

        double megabytes = Math.ceil((double) bytes / 1024 / 1024);
        return (int) megabytes;
    }

    /**
     * Save in disk the object passed.
     *
     * @param key         the key whereby the object could be retrieved/deleted later. @see evict and @see
     *                    retrieve.
     * @param data        the object to be persisted.
     * @param isEncrypted If the persisted record is encrypted or not.
     * @param encryptKey  The key used to encrypt/decrypt the record to be persisted.
     */
    public void save(String key, Object data, boolean isEncrypted, String encryptKey) {
        key = safetyKey(key);

        String wrapperJSONSerialized;

        if (data instanceof Record) {
            Type type = jolyglot.newParameterizedType(data.getClass(), Object.class);
            wrapperJSONSerialized = jolyglot.toJson(data, type);
        } else {
            wrapperJSONSerialized = jolyglot.toJson(data);
        }

        FileWriter fileWriter = null;

        try {
            File file = new File(cacheDirectory, key);
            fileWriter = new FileWriter(file, false);
            fileWriter.write(wrapperJSONSerialized);
            fileWriter.flush();
            fileWriter.close();
            fileWriter = null;

            if (isEncrypted) {
                fileEncryptor.encrypt(encryptKey, new File(cacheDirectory, key));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (fileWriter != null) {
                    fileWriter.flush();
                    fileWriter.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Delete the object previously saved.
     *
     * @param key the key whereby the object could be deleted.
     */
    @Override
    public void evict(String key) {
        key = safetyKey(key);
        final File file = new File(cacheDirectory, key);
        boolean isDelete = file.delete();
        if (!isDelete) {
            HiLog.error(LABEL, "delete failed");
        }
    }

    /**
     * Delete all objects previously saved.
     */
    @Override
    public void evictAll() {
        File[] files = cacheDirectory.listFiles();
        boolean isDelete = false;
        if (null != files) {
            for (File file : files) {
                if (file != null)
                    isDelete = file.delete();
            }
        }
        if (!isDelete) {
            HiLog.error(LABEL, "delete failed");
        }

    }

    /**
     * retrieve
     * Retrieve the object previously saved.
     *
     * @param key         the key whereby the object could be retrieved.
     * @param clazz       the type of class against the object need to be serialized
     * @param isEncrypted If the persisted record is encrypted or not.
     * @param encryptKey  The key used to encrypt/decrypt the record to be persisted.
     * @return T
     */
    public <T> T retrieve(String key, final Class<T> clazz, boolean isEncrypted, String encryptKey) {
        key = safetyKey(key);
        boolean isDelete = false;
        File file = new File(cacheDirectory, key);

        if (isEncrypted) {
            file = fileEncryptor.decrypt(encryptKey, file);
        }

        try {
            T data = jolyglot.fromJson(file, clazz);
            return data;
        } catch (Exception ignore) {
            return null;
        } finally {
            if (isEncrypted) {
                isDelete = file.delete();
                if (!isDelete) {
                    HiLog.error(LABEL, "delete failed");
                }
            }
        }
    }

    /**
     * retrieveRecord
     * Retrieve the Record previously saved.
     *
     * @param key         the key whereby the object could be retrieved.
     * @param isEncrypted If the persisted record is encrypted or not.
     * @param encryptKey  The key used to encrypt/decrypt the record to be persisted.
     * @return Record<T>
     */
    @Override
    public <T> Record<T> retrieveRecord(String key, boolean isEncrypted,
                                        String encryptKey) {
        key = safetyKey(key);
        boolean isDelete = false;
        File file = new File(cacheDirectory, key);

        try {
            if (isEncrypted) {
                file = fileEncryptor.decrypt(encryptKey, file);
            }

            Type partialType = jolyglot.newParameterizedType(Record.class, Object.class);
            Record tempDiskRecord = jolyglot.fromJson(file, partialType);

            Class classData = Class.forName(tempDiskRecord.getDataClassName());
            Class classCollectionData = tempDiskRecord.getDataCollectionClassName() == null
                    ? Object.class : Class.forName(tempDiskRecord.getDataCollectionClassName());

            boolean isCollection = Collection.class.isAssignableFrom(classCollectionData);
            boolean isArray = classCollectionData.isArray();
            boolean isMap = Map.class.isAssignableFrom(classCollectionData);
            Record<T> diskRecord;

            if (isCollection) {
                Type typeCollection = jolyglot.newParameterizedType(classCollectionData, classData);
                Type typeRecord = jolyglot.newParameterizedType(Record.class, typeCollection);
                diskRecord = jolyglot.fromJson(file.getAbsoluteFile(), typeRecord);
            } else if (isArray) {
                Type typeRecord = jolyglot.newParameterizedType(Record.class, classCollectionData);
                diskRecord = jolyglot.fromJson(file.getAbsoluteFile(), typeRecord);
            } else if (isMap) {
                Class classKeyMap = Class.forName(tempDiskRecord.getDataKeyMapClassName());
                Type typeMap = jolyglot.newParameterizedType(classCollectionData, classKeyMap, classData);
                Type typeRecord = jolyglot.newParameterizedType(Record.class, typeMap);
                diskRecord = jolyglot.fromJson(file.getAbsoluteFile(), typeRecord);
            } else {
                Type type = jolyglot.newParameterizedType(Record.class, classData);
                diskRecord = jolyglot.fromJson(file.getAbsoluteFile(), type);
            }

            diskRecord.setSizeOnMb(file.length() / 1024f / 1024f);

            return diskRecord;
        } catch (Exception ignore) {
            return null;
        } finally {
            if (isEncrypted) {
                isDelete = file.delete();
                if (!isDelete) {
                    HiLog.error(LABEL, "delete failed");
                }
            }
        }
    }

    /**
     * retrieveCollection
     * Retrieve a collection previously saved.
     *
     * @param key             the key whereby the object could be retrieved.
     * @param classCollection type class collection
     * @param classData       type class contained by the collection, not the collection itself
     * @return C
     */
    public <C extends Collection<T>, T> C retrieveCollection(String key, Class<C> classCollection,
                                                             Class<T> classData) {
        key = safetyKey(key);

        try {
            File file = new File(cacheDirectory, key);
            Type typeCollection = jolyglot.newParameterizedType(classCollection, classData);
            T data = jolyglot.fromJson(file, typeCollection);
            return (C) data;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * retrieveMap
     * Retrieve a Map previously saved.
     *
     * @param key           the key whereby the object could be retrieved.
     * @param classMap      type class Map
     * @param classMapKey   type class of the Map key
     * @param classMapValue type class of the Map value
     * @return M
     */
    public <M extends Map<K, V>, K, V> M retrieveMap(String key, Class classMap, Class<K> classMapKey,
                                                     Class<V> classMapValue) {
        key = safetyKey(key);

        try {
            File file = new File(cacheDirectory, key);

            Type typeMap = jolyglot.newParameterizedType(classMap, classMapKey, classMapValue);
            Object data = jolyglot.fromJson(file, typeMap);

            return (M) data;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * retrieveArray
     * Retrieve an Array previously saved.
     *
     * @param key       the key whereby the object could be retrieved.
     * @param classData type class contained by the Array
     * @return T[]
     */
    public <T> T[] retrieveArray(String key, Class<T> classData) {
        key = safetyKey(key);

        try {
            File file = new File(cacheDirectory, key);

            Class<?> clazzArray = Array.newInstance(classData, 1).getClass();
            Object data = jolyglot.fromJson(file, clazzArray);

            return (T[]) data;
        } catch (Exception e) {
            return null;
        }
    }

    private String safetyKey(String key) {
        return key.replaceAll("/", "_");
    }
}